package Neo4j;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Random;

import org.neo4j.tooling.GlobalGraphOperations;
import org.neo4j.kernel.KernelData;
import org.neo4j.cypher.ExecutionEngine;
import org.neo4j.cypher.ExecutionResult;
import org.neo4j.graphalgo.GraphAlgoFactory;
import org.neo4j.graphalgo.PathFinder;
import org.neo4j.graphalgo.WeightedPath;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.index.*;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.factory.GraphDatabaseFactory;
import org.neo4j.graphdb.factory.GraphDatabaseSetting;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.graphdb.traversal.Evaluators;
import org.neo4j.graphdb.traversal.TraversalDescription;
import org.neo4j.jmx.Kernel;
import org.neo4j.kernel.AbstractGraphDatabase;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.kernel.Traversal;
import org.neo4j.kernel.impl.core.NodeManager;
import org.neo4j.kernel.impl.util.FileUtils;


/**
 * Class to demonstrate Neo4j
 *
 */
public class EmbeddedNeo4j
{
	//
	// Member variables
	//
    private static final String DB_PATH = "target/neo4j-db";
    private static final Random randomGenerator = new Random();
    private static final Integer maxRandomAge = 100;
    private NodeManager nm;
    GraphDatabaseService graphDb;
    private Index<Node> ids;
    Node firstNode;
    Node secondNode;
    Relationship relationship;
    // Count of relationships processed
    int relationshipsProcessed = 0;
    private static String graphAlgorithm = "";
    private String outputFileName = "";
    // Map that maps the UID of a node to its internal ID (long)
	ArrayList<Long> nodeList = new ArrayList<Long>();
    // List of IDs that correspond to the left column of uids
	//ArrayList<Long> leftNodeList = new ArrayList<Long> ();
	
	// Variables to control how much data to read in and how many data points to make
	static int numRelationshipsToProcess = 1500000;
	static int numDataPointsToCreate = 100;

    /**
     * Enumeration for the relationship types
     */
    private static enum RelTypes implements RelationshipType
    {
        CONNECTED,
    }
    

    /**
     * Main
     */
    public static void main( final String[] args )
    {
    	// Check for the correct number of arguments to be passed in
        if (args.length < 5) {
            // Print out an error message to the screen
            System.out.println("Error: Incorrect number of input parameters!");
            // Call function to display usage
            DisplayUsage();
            // Exit the program
            System.exit(0);
        }
        
        // Parse input params
        Integer create_db = Integer.parseInt(args[0]);
        String create_data = args[1];
        graphAlgorithm = args[2];
        numRelationshipsToProcess = Integer.parseInt(args[3]);
        numDataPointsToCreate = Integer.parseInt(args[4]);
        
        System.out.println("Running Neo4j as follows:");
        System.out.println("\tCreate database: " + create_db);
        System.out.println("\tCreate output data: " + create_data);
        System.out.println("\tUse graph algorithm: " + graphAlgorithm);
        System.out.println("\tNumber of relationships to read in: " + numRelationshipsToProcess);
        System.out.println("\tNumber of output data points to make: " + numDataPointsToCreate);
        System.out.println("");
    	
    	// Create a new embeddedNeo4j object
        EmbeddedNeo4j neo4j = new EmbeddedNeo4j();
        
        // Initialize the database
        neo4j.initDb();
        
        // Check if we were told to create the database
        if (create_db == 1) 
        {
        	System.out.println("Creating new database ...");
        	 
        	// Create the graph database
        	neo4j.createDb();
        }     
        else
        {
        	System.out.println("Not creating a new database, using existing ...");
        }
        
        
        // look for test
        // Check if we were told to create the output data
        if (create_data.equals("shortest_path")) 
        {
        	System.out.println("Creating output data ...");
        	
        	// Create the output shortest path data
        	neo4j.createShortestPathData();
        }
        else if (create_data.equals("query"))
        {
        	neo4j.createQueryData();
        }
        else if (create_data.equals("queryAge"))
        {
        	neo4j.createQueryData();
        }
        else 
        {
        	System.out.println("Not creating output data ...");
        }
        
        // Shutdown
        neo4j.shutDown();
    }
    
    public void initDb(){
        graphDb = new GraphDatabaseFactory().
    		    newEmbeddedDatabaseBuilder( DB_PATH ).
    		    setConfig( GraphDatabaseSettings.node_keys_indexable, "id" ).
    		    setConfig( GraphDatabaseSettings.node_auto_indexing, GraphDatabaseSetting.TRUE ).newGraphDatabase();

        // Register the shutdown hook
        registerShutdownHook( graphDb );
        
        // populate node list
        Iterator<Node> it  = GlobalGraphOperations.at(graphDb).getAllNodes().iterator();
        System.out.println("iterating nodes");
        while(it.hasNext()){
        	
        	nodeList.add(it.next().getId());
        }
    }

    /**
     * Function to create the graph database 
     */
    public void createDb()
    {
    	// Local variables
    	Integer count = 0;
    	Integer uid1 = 0;
		Integer uid2 = 0;
		String timestamp = "";
    	Node firstNode = null;
    	Node secondNode = null;
    	String strLine;
		
    	// Clear out the database contents (if anything existed before)
        //clearDb();

    	// Create the database index
		ReadableIndex<Node> autoNodeIndex = graphDb.index()
		        .getNodeAutoIndexer()
		        .getAutoIndex();

        // Begin a transaction
        Transaction tx = graphDb.beginTx();
        try
        {
			// Open the input data file
			FileInputStream fstream;
			try 
			{
				fstream = new FileInputStream("data/facebook-links.txt.anon");
				// Get the object of DataInputStream
				DataInputStream in = new DataInputStream(fstream);
				BufferedReader br = new BufferedReader(new InputStreamReader(in));
				
				// Read the file line by line
				System.out.println("* Creating nodes and relationships ...");
				while (((strLine = br.readLine()) != null) && relationshipsProcessed < numRelationshipsToProcess)
				{		
					// Split the line into the 3 columnar data items (by tab)
					String[] pieces = strLine.split("\t");
					
					// Assign split string members
					uid1 = Integer.parseInt(pieces[0]);
					uid2 = Integer.parseInt(pieces[1]);					
					timestamp = pieces[2];
					
					// Check if the timestamp is '\N'
					if (timestamp.equals("\\N")) 
					{
						// Set timestamp to be an empty string
						timestamp = "";
					}
					
					// Check if haven't already created the node corresponding to uid1
					firstNode = autoNodeIndex.get( "id", Integer.toString(uid1)).getSingle();
					if (firstNode == null) 
					{
						// Create the node corresponding to uid1 and set its uid property
						firstNode = graphDb.createNode();
						firstNode.setProperty("id", uid1);
						
						// Add a random age to the node
						addRandomPropertyAgeToNode(firstNode, maxRandomAge);
						
						// Add the node's information to the nodeList
						nodeList.add(firstNode.getId());
					}

					secondNode = autoNodeIndex.get( "id", Integer.toString(uid2)).getSingle();
					// Check if we haven't already created the node corresponding to uid2
					if (secondNode == null) {
						// Create the node corresponding to uid2 and set its uid property
						secondNode = graphDb.createNode();
						secondNode.setProperty("id", uid2);
						
						// Add a random age to the node
						addRandomPropertyAgeToNode(secondNode, maxRandomAge);
						
						// Add the node's information to the nodeList
						nodeList.add(secondNode.getId());
					}


		            // Create the relationship between the two nodes (they are connected)
		            relationship = firstNode.createRelationshipTo(secondNode, RelTypes.CONNECTED);
		            
		            // Set the time property to be the timestamp from the input data
		            relationship.setProperty("time", timestamp);
		            
		            // Set the cost property to be 1
		            // NOTE: We do this for calculating cost-based paths, each path in our 
		            //		 system costs the same, so they are all set to 1
		            relationship.setProperty("cost", 1);
		            
		            // Increment relationships processed count
		            relationshipsProcessed++;
		            //System.out.println("Relationships processed: " + relationshipsProcessed + ", nodelist size: " + nodeList.size());
		            
		            // Commit each 50,000 relationships
		            if (relationshipsProcessed % 5000 == 0)
		            {
		            	System.out.println("Committing transaction ...");
		            	
		            	// Mark the transaction a success, and finish it (commit it)
		            	tx.success();
		            	tx.finish();
		            	
		            	System.out.println("Beginning transaction ...");
		            	tx = graphDb.beginTx();
		            }
		            
		            //System.out.println("Created a connected relationship from: " + firstNode.getProperty("uid") + " to: " + secondNode.getProperty("uid") + ", time: " + relationship.getProperty("time"));
				}
				
				// Close the input stream
				in.close();	
			} 
			catch (FileNotFoundException e) 
			{
				System.out.println("Error: Couldn't find input file");
				e.printStackTrace();
			}
			catch (IOException e) 
			{
				System.out.println("Error: I/O exception");
				e.printStackTrace();
			}
				
			// Print out the size of the map
			System.out.println("");
			System.out.println("* Displaying summary statistics ...");
			System.out.println("Number of nodes in the nodeList map: " + nodeList.size());
			System.out.println("");
			
			// Mark the transaction as successful
			tx.success();			
        }
        finally
        {
        	// Commit the final transaction
            tx.finish();
        }
        // END SNIPPET: transaction         
    }
    
    /**
     * Function to add an 'age' property to a node (random between [0,maxAge]
     * 
     * @param node The node to add the age to
     * @param maxAge The maximum age
     */
    private void addRandomPropertyAgeToNode(Node node, Integer maxAge) 
    {
    	// Generate a random number [0,maxAge]
    	Integer randomAge = this.randomGenerator.nextInt(maxAge);
    	
    	// Add the 'age' property to the node
    	node.setProperty("age", randomAge);
    }
    
    /**
     * Function to create random query data
     */
    public void createQueryData(){
    	System.out.println("createQueryData");
    	ExecutionEngine engine = new ExecutionEngine( graphDb );
    	
    	for(int i = 0 ; i < numDataPointsToCreate; i++){
    		long nodeID = nodeList.get(randomGenerator.nextInt(nodeList.size()));
        	String query = "START n=node:node_auto_index(id=\"" + nodeID +"\") RETURN n";  
        	//System.out.println("query: " + query);
        	long start = System.nanoTime();
        	ExecutionResult result = engine.execute( query );
        	long elapsedTime = System.nanoTime() - start;
        	System.out.println("Query " + i + " " + nodeID + " time: " + elapsedTime);
        	int minAge = this.randomGenerator.nextInt(80);
        	query = "START x=node(*) WHERE (x.age? > " + minAge + " and x.age? < " + minAge + 20 +" ) return count(*);";
        	start = System.nanoTime();
        	result = engine.execute( query );
        	elapsedTime = System.nanoTime() - start;
        	System.out.println("Query " + i + " " + nodeID + " time: " + elapsedTime);
    	}
    }
    
    /**
     * Function to create query data for people in a given age range
     */
    public void createAgeRangeQueryData (Integer minAge, Integer maxAge)
    {
    	System.out.println("createAgeRangeQueryData");
    	ExecutionEngine engine = new ExecutionEngine( graphDb );
    	
    	// Loop over how many data points to make
    	for (int i = 0 ; i < numDataPointsToCreate; i++)
    	{
    		long nodeID = nodeList.get(randomGenerator.nextInt(nodeList.size()));
    		
    		// Create the query string
        	String query = "START x=node(*) return x;";
        	
        	System.out.println("query: " + query);
        	
        	// Start the timer
        	long start = System.nanoTime();
        	// Execute the query
        	ExecutionResult result = engine.execute( query );
        	// Stop the timer
        	long elapsedTime = System.nanoTime() - start;
        	
        	// Output info
        	System.out.println("Query " + i + " " + nodeID + " time: " + elapsedTime);        	        
    	}
    }
    
    /**
     * Function to create the data needed to show performance of the shortest path algorithm (Dijkstra's)
     */
    public void createShortestPathData() 
    {
    	System.out.println("Creating data output for Dijkstra's shortest path algorithm ...");
    	
    	// Member variables
        FileWriter fStream = null;
        BufferedWriter out = null;
        Node sourceNode = null;
        Node destNode = null;
        String outputLine = "";
        Path shortestPath = null;
        
        // Try to create the output data file
        try 
        {
        	outputFileName = "output/Neo4j_" + graphAlgorithm + "_path_output.txt";
        	fStream = new FileWriter(outputFileName);
            out = new BufferedWriter(fStream);
            out.write("Num_Relationships|Source_Node|Dest_Node|Distance|Elapsed_Time (ns)\n");
        }
        catch (Exception e) {
            System.err.println("Error opening file: " + e.getMessage());
        }
        
        try 
        {
        	// Generate 1000 data points
        	for (int i = 0; i < numDataPointsToCreate; i++) 
        	{
        		// Grab a unique source and destination node (to calculate shortest path between)        	
        		// Get the node objects for the indexes
        		long nodeID = nodeList.get(randomGenerator.nextInt(nodeList.size()));
        		System.out.println("source node " + nodeList.get(randomGenerator.nextInt(nodeList.size())));
        		sourceNode = graphDb.getNodeById(nodeID);
        		
        		nodeID = nodeList.get(randomGenerator.nextInt(nodeList.size()));
        		destNode = graphDb.getNodeById(nodeID);
        		
        		System.out.println("Getting shortest path info for srcNode: " + sourceNode.getProperty("id") + ", destNode: " + destNode.getProperty("id") );
        		
        		// Start a timer 
    			long start = System.nanoTime();
    			
        		// Calculate the shortest path between the two nodes (based on algorithm we were configured with)
    			if (graphAlgorithm == "dijkstra") 
    			{
    				shortestPath = this.findDijkstrasShortestPath(sourceNode, destNode);			
    			}
    			else if (graphAlgorithm.equals("shortest"))
    			{
    				shortestPath = this.findShortestPath(sourceNode, destNode);
    			}
    			else {
    				System.out.println("Error: Unhandled algorithm type: " + graphAlgorithm);
    				System.exit(0);
    			}
    			
    			// Stop the timer
    			long elapsedTime = System.nanoTime() - start;
    			System.out.println("Finding shortest path took: " + elapsedTime + " (ns)");
    			
				// Check if a path existed
    			if (shortestPath != null) 
    			{
    				// Print out the path information
    				this.printPathInformation(shortestPath);
    				
    				// Create the output line data
    				outputLine = numRelationshipsToProcess + "|" + sourceNode.getProperty("id") + "|" + destNode.getProperty("id") + "|" + shortestPath.length() + "|" + elapsedTime + "\n";
    			}
    			else
    			{
    				System.out.println("No path existed ...");
    				
    				// Create the output line data
    				outputLine = numRelationshipsToProcess + "|" + sourceNode.getProperty("id") + "|" + destNode.getProperty("id") + "|0|" + elapsedTime + "\n";
    			}
    			
    			// Output csv-style data to file
                try 
                {
                	if (out != null) 
                	{
                		out.write(outputLine);
                		out.flush();
                    }
                }
                catch (Exception e) 
                {
                	System.err.println("Error writing: " + e.getMessage());
                }  
        	}
        }
        catch (Exception e) 
        {
        	System.out.println("Exception: " + e);
        	e.printStackTrace();
        }
    }
    
    /**
     * Function to calculate the person who has the least friends in the system
     */
    /*
    private void calculateLeastPopularPerson()
    {
    	System.out.println("* Calculating least popular person .......");
    	
    	Node leastPopularNode = null;
    	Integer leastFriends = Integer.MAX_VALUE;
    	
    	// Loop over the list of left nodes
		Node leftNode = null;
		for (int i = 0; i < leftNodeList.size(); i++) 
		{
			Integer numFriends = 0;
			
			// Get the node corresponding to the ID
			leftNode = graphDb.getNodeById(leftNodeList.get(i));
			
			// Get a Traverser object on the node's friends
			org.neo4j.graphdb.traversal.Traverser friendsTraverser = getFriends( leftNode );
			
			// Loop over the friends in the traverser object
			for ( Path friendPath : friendsTraverser )
			{		    
				numFriends++;
			}
			
			// Check if this node is more popular
			if (numFriends < leastFriends) {
				// Set the least popular node and the new minimum number of friends
				leastPopularNode = leftNode;
				leastFriends = numFriends;
			}
		}
		
		System.out.println("Least popular node is: " + leastPopularNode.getProperty("uid") + " with: " + leastFriends + " friends");
    }
    */
    
    /**
     * Function to calculate the person who has the most friends in the system
     */
    /*
    private void calculateMostPopularPerson()
    {
    	System.out.println("* Calculating most popular person .......");
    	
    	Node mostPopularNode = null;
    	Integer mostFriends = 0;
    	
    	// Loop over the list of left nodes
		Node leftNode = null;
		for (int i = 0; i < leftNodeList.size(); i++) 
		{
			Integer numFriends = 0;
			
			// Get the node corresponding to the ID
			leftNode = graphDb.getNodeById(leftNodeList.get(i));
			
			// Get a Traverser object on the node's friends
			org.neo4j.graphdb.traversal.Traverser friendsTraverser = getFriends( leftNode );
			
			// Loop over the friends in the traverser object
			for ( Path friendPath : friendsTraverser )
			{		    
				numFriends++;
			}
			
			// Check if this node is more popular
			if (numFriends > mostFriends) {
				// Set the most popular node and the new maximum number of friends
				mostPopularNode = leftNode;
				mostFriends = numFriends;
			}
			
			System.out.println("Node: " + leftNode.getProperty("uid") + " has: " + numFriends + " friends.");
		}
		
		System.out.println("Most popular node is: " + mostPopularNode.getProperty("uid") + " with: " + mostFriends + " friends");
    }    
    */
    
    /**
     * Function to print out the pertinent information about a weighted path
     * 
     * @param path the path object to print out
     */
    private void printWeightedPathInformation(WeightedPath path) 
    {
    	// Print out the weight of the path
    	System.out.println("Weight of path: " + path.weight());

    	// Get the length of the path
    	Integer pathLength = path.length();
    	
    	int i = 0;
    	
    	System.out.print("Path is: ");
    	// Loop over the nodes in the path    	
    	for (Node node : path.nodes()) 
    	{
    		if (i < pathLength)
    		{
    			System.out.print("Node " + node.getProperty("id") + " --> ");
    		}
    		else 
    		{
    			System.out.print("Node " + node.getProperty("id"));
    		}
    		
    		i++;
    	}
    	System.out.println("");
    }
    
    /**
     * Function to print out the pertinent information about a path
     * 
     * @param path the path object to print out
     */
    private void printPathInformation(Path path) 
    {
    	// Get the length of the path
    	Integer pathLength = path.length();
    	
    	int i = 0;
    	
    	System.out.print("Path is: ");
    	// Loop over the nodes in the path    	
    	for (Node node : path.nodes()) 
    	{
    		if (i < pathLength)
    		{
    			System.out.print("Node " + node.getProperty("id") + " --> ");
    		}
    		else 
    		{
    			System.out.print("Node " + node.getProperty("id"));
    		}
    		
    		i++;
    	}
    	System.out.println("");
    }

    /**
     * Function to return the path of the shortest path (Dijkstra's algorithm) between two nodes in the system. 
     * 
     * @param sourceNode the source node of the path
     * @param destNost the destination node of the path
     * @return the weighted path from source node to destination node
     */
    private WeightedPath findDijkstrasShortestPath(Node sourceNode, Node destNode) 
    {
    	// Use Dijkstra's algorithm to create the Path Finder object
    	PathFinder<WeightedPath> finder = GraphAlgoFactory.dijkstra(
		        Traversal.expanderForTypes( RelTypes.CONNECTED, Direction.BOTH ), "cost" );
		 
    	// Find a single path from source node to dest node
		WeightedPath path = finder.findSinglePath( sourceNode, destNode );
		
		return path;
    }
    
    /**
     * Function to return the path of the shortest path between two nodes in the system. 
     * 
     * @param sourceNode the source node of the path
     * @param destNost the destination node of the path
     * @return the weighted path from source node to destination node
     */
    private Path findShortestPath(Node sourceNode, Node destNode) 
    {   	
    	// Use shortest path algorithm to create the Path Finder object
    	// NOTE: Max depth is set to 10 (shouldn't have any higher than this with our data)
    	PathFinder<Path> finder = GraphAlgoFactory.shortestPath(
    			Traversal.expanderForTypes( RelTypes.CONNECTED, Direction.BOTH ), 10);
		 
    	// Find a single path from source node to dest node
		Path path = finder.findSinglePath( sourceNode, destNode );
		
		return path;
    }
    
    /**
     * Function to print the friend information for a given node
     * 
     * @param person the person to find the friends of
     */
    private void printFriendInformation(final Node person) 
    {
    	// Variable for number of friends found
    	int numberOfFriends = 0;
    	
    	// Build up initial part of output string
		String output = person.getProperty( "uid" ) + "'s friends:\n";
		
		// Call helper function to get the friends and do the actual graph traversal
		org.neo4j.graphdb.traversal.Traverser friendsTraverser = getFriends( person );
		
		// Loop over the friends in the traverser object
		for ( Path friendPath : friendsTraverser )
		{
			// Build up the output string
		    output += "\tAt depth " + friendPath.length() + " => "
		              + friendPath.endNode().getProperty( "id" ) + "\n";
		    
		    numberOfFriends++;
		}
		
		// Build up output string
		output += "Number of friends found: " + numberOfFriends + "\n";
		
		System.out.println(output);
    }
    
    /**
     * Function to get friends of a given node
     * 
     * @param person The node to find the friends of
     * @return Traverser object to traverse and deal with friend information
     */
    private static org.neo4j.graphdb.traversal.Traverser getFriends(final Node person)
    {
        TraversalDescription td = Traversal.description()
                .breadthFirst()
                .relationships( RelTypes.CONNECTED, Direction.OUTGOING )
                .evaluator( Evaluators.excludeStartPosition() );
        return td.traverse( person );
    }
    
    /**
     * Function to clear the contents of the graph database
     */
    private void clearDb()
    {
    	// Try to recursively delete files in the path of the graph database
        try
        {
            FileUtils.deleteRecursively( new File( DB_PATH ) );
        }
        catch ( IOException e )
        {
            throw new RuntimeException( e );
        }
    }

    /**
     * Function that is called during shutdown
     */
    void shutDown()
    {
    	// Print out a message
    	System.out.println("");
        System.out.println("Shutting down database ...");
        
        // Shut down the database
        graphDb.shutdown();
    }

    /**
     * Function to register the hook method for shutdown
     * @param graphDb
     */
    private static void registerShutdownHook( final GraphDatabaseService graphDb )
    {
        // Registers a shutdown hook for the Neo4j instance so that it
        // shuts down nicely when the VM exits (even if you "Ctrl-C" the
        // running example before it's completed)
        Runtime.getRuntime().addShutdownHook( new Thread()
        {
            @Override
            public void run()
            {
                graphDb.shutdown();
            }
        } );
    }
    
    /**
     * Function to display the usage
     */
    public static void DisplayUsage() {
        System.out.println("\nEmbeddedNeo4j - Used to launch an embedded Neo4j client\n");
        System.out.println("\tUsage: ./EmbeddedNeo4j <create_db> <create_data> <path_algo> <num_relationships> <num_data_points>");
        System.out.println("");
        System.out.println("\tWhere:");
        System.out.println("\t\t<create_db>: determines whether or not to create the database (1 for yes, 0 for no)");
        System.out.println("\t\t<create_data>: determines whether or not to create the output path data (shortest_path | query | none)");
        System.out.println("\t\t<graph_algo>: determines which graph algorithm to utilize (set to: 'dijkstra' or 'shortest')");
        System.out.println("\t\t<num_relationships>: the number of relationships to use from the input file");
        System.out.println("\t\t<num_data_points>: the number of output data points to create (if <create_data> was set to 1)");
    }
}
